/*
 * Copyright (C) 2012 Brockmann Consult GmbH (info@brockmann-consult.de)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or (at your option)
 * any later version.
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, see http://www.gnu.org/licenses/
 */

package org.esa.snap.core.gpf.ui;

import com.bc.ceres.swing.TableLayout;
import com.bc.ceres.swing.selection.SelectionChangeListener;
import com.bc.ceres.swing.selection.support.ComboBoxSelectionContext;
import org.esa.snap.core.dataio.ProductIO;
import org.esa.snap.core.dataio.ProductIOPlugInManager;
import org.esa.snap.core.dataio.ProductReaderPlugIn;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.datamodel.ProductFilter;
import org.esa.snap.core.datamodel.ProductManager;
import org.esa.snap.core.datamodel.ProductNode;
import org.esa.snap.core.util.StringUtils;
import org.esa.snap.core.util.SystemUtils;
import org.esa.snap.core.util.io.SnapFileFilter;
import org.esa.snap.rcp.actions.file.OpenProductAction;
import org.esa.snap.ui.AbstractDialog;
import org.esa.snap.ui.AppContext;
import org.esa.snap.ui.SnapFileChooser;
import org.openide.util.Utilities;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;

/**
 * WARNING: This class belongs to a preliminary API and may change in future releases.
 * todo - add capability to select/add/remove multiple soures (nf - 27.11.2007)
 * todo - add capability to specify optional sources
 *
 * @author Ralf Quast
 * @version $Revision$ $Date$
 */
public class SourceProductSelector {

    private AppContext appContext;
    private ProductFilter productFilter;
    private Product extraProduct;
    private File currentDirectory;
    private DefaultComboBoxModel<Object> productListModel;
    private JLabel productNameLabel;
    private JButton productFileChooserButton;
    private JComboBox<Object> productNameComboBox;
    private final ProductManager.Listener productManagerListener;
    private ComboBoxSelectionContext selectionContext;
    private boolean enableEmptySelection;

    public SourceProductSelector(AppContext appContext) {
        this(appContext, false);
    }

    public SourceProductSelector(AppContext appContext, boolean enableEmptySelection) {
        this(appContext, "Name:", enableEmptySelection);
    }

    public SourceProductSelector(AppContext appContext, String labelText) {
        this(appContext, labelText, false);
    }

    public SourceProductSelector(AppContext appContext, String labelText, boolean enableEmptySelection) {
        this.appContext = appContext;
        this.enableEmptySelection = enableEmptySelection;

        productListModel = new DefaultComboBoxModel<>();

        productNameLabel = new JLabel(labelText);
        productFileChooserButton = new JButton(new ProductFileChooserAction());
        final Dimension size = new Dimension(26, 16);
        productFileChooserButton.setPreferredSize(size);
        productFileChooserButton.setMinimumSize(size);

        productNameComboBox = new JComboBox<>(productListModel);
        productNameComboBox.setPrototypeDisplayValue("[1] 123456789 123456789 12345");
        productNameComboBox.setRenderer(new ProductListCellRenderer());
        productNameComboBox.addPopupMenuListener(new ProductPopupMenuListener());
        productNameComboBox.addActionListener(e -> {
            final Object selected = productNameComboBox.getSelectedItem();
            if (selected != null && selected instanceof Product) {
                Product product = (Product) selected;
                if (product.getFileLocation() != null) {
                    productNameComboBox.setToolTipText(product.getFileLocation().getPath());
                } else {
                    productNameComboBox.setToolTipText(product.getDisplayName());
                }
            } else {
                productNameComboBox.setToolTipText("Select a source product.");
            }
        });

        productFilter = ProductFilter.ALL;
        selectionContext = new ComboBoxSelectionContext(productNameComboBox);

        productManagerListener = new ProductManager.Listener() {
            @Override
            public void productAdded(ProductManager.Event event) {
                addProduct(event.getProduct());
            }

            @Override
            public void productRemoved(ProductManager.Event event) {
                Product product = event.getProduct();
                if (productListModel.getSelectedItem() == product) {
                    productListModel.setSelectedItem(null);
                }
                productListModel.removeElement(product);
            }
        };
    }

    /**
     * @return the product filter, default is a filter which accepts all products
     */
    public ProductFilter getProductFilter() {
        return productFilter;
    }

    /**
     * @param productFilter the product filter
     */
    public void setProductFilter(ProductFilter productFilter) {
        this.productFilter = productFilter;
    }

    public synchronized void initProducts() {
        productListModel.removeAllElements();
        if (enableEmptySelection) {
            productListModel.addElement(null);
        }
        for (Product product : appContext.getProductManager().getProducts()) {
            addProduct(product);
        }

        Product selectedProduct = appContext.getSelectedProduct();
        final ProductNode productNode = Utilities.actionsGlobalContext().lookup(ProductNode.class);
        if (productNode != null) {
            // user would want to apply operation to the selected productNode rather than the productSceneView
            selectedProduct = productNode.getProduct();
        }

        if (enableEmptySelection) {
            productListModel.setSelectedItem(null);
        } else if (selectedProduct != null && productFilter.accept(selectedProduct)) {
            productListModel.setSelectedItem(selectedProduct);
        }

        appContext.getProductManager().addListener(productManagerListener);
    }

    public int getProductCount() {
        if (enableEmptySelection) {
            return productListModel.getSize() - 1;
        } else {
            return productListModel.getSize();
        }
    }

    public void setSelectedIndex(int index) {
        productListModel.setSelectedItem(productListModel.getElementAt(index));
    }

    public Product getSelectedProduct() {
        return (Product) productListModel.getSelectedItem();
    }

    public void setCurrentDirectory(File directory) {
        if (directory != null && directory.isDirectory()) {
            currentDirectory = directory;
        }
    }

    public File getCurrentDirectory() {
        return currentDirectory;
    }

    public void setSelectedProduct(Product product) {
        if (product == null) {
            productListModel.setSelectedItem(null);
            return;
        }
        if (productListModelContains(product)) {
            productListModel.setSelectedItem(product);
        } else {
            if (productFilter.accept(product)) {
                if (extraProduct != null) {
                    productListModel.removeElement(extraProduct);
                    extraProduct.dispose();
                }
                productListModel.addElement(product);
                productListModel.setSelectedItem(product);
                extraProduct = product;
            }
        }
    }

    public synchronized void releaseProducts() {
        appContext.getProductManager().removeListener(productManagerListener);
        if (extraProduct != null && getSelectedProduct() != extraProduct) {
            extraProduct.dispose();
        }
        extraProduct = null;
        productListModel.removeAllElements();
    }

    public void addSelectionChangeListener(SelectionChangeListener listener) {
        selectionContext.addSelectionChangeListener(listener);
    }

    public void removeSelectionChangeListener(SelectionChangeListener listener) {
        selectionContext.removeSelectionChangeListener(listener);
    }

    private void addProduct(Product product) {
        if (productFilter.accept(product)) {
            productListModel.addElement(product);
        }
    }

    // UI Components

    /////////////////////////////////////

    public JComboBox<Object> getProductNameComboBox() {
        return productNameComboBox;
    }

    public JLabel getProductNameLabel() {
        return productNameLabel;
    }

    public JButton getProductFileChooserButton() {
        return productFileChooserButton;
    }

    private boolean productListModelContains(Product product) {
        for (int i = 0; i < productListModel.getSize(); i++) {
            Object listProduct = productListModel.getElementAt(i);
            if (listProduct == null) {
                continue;
            }
            if (listProduct.equals(product)) {
                return true;
            }
        }
        return false;
    }

    public JPanel createDefaultPanel() {
        return createDefaultPanel("Source Product");
    }

    public JPanel createDefaultPanel(String borderTitle) {
        final JPanel subPanel = new JPanel(new BorderLayout(3, 3));
        subPanel.add(getProductNameComboBox(), BorderLayout.CENTER);
        subPanel.add(getProductFileChooserButton(), BorderLayout.EAST);

        final TableLayout tableLayout = new TableLayout(1);
        tableLayout.setTableAnchor(TableLayout.Anchor.WEST);
        tableLayout.setTableWeightX(1.0);
        tableLayout.setRowFill(0, TableLayout.Fill.HORIZONTAL);
        tableLayout.setRowFill(1, TableLayout.Fill.HORIZONTAL);
        tableLayout.setTablePadding(3, 3);
        JPanel panel = new JPanel(tableLayout);
        panel.add(getProductNameLabel());
        panel.add(subPanel);
        if (StringUtils.isNotNullAndNotEmpty(borderTitle)) {
            panel.setBorder(BorderFactory.createTitledBorder(borderTitle));
            panel.add(tableLayout.createVerticalSpacer());
        }
        return panel;
    }

    private class ProductFileChooserAction extends AbstractAction {

        private String APPROVE_BUTTON_TEXT = "Select";
        private JFileChooser chooser;

        private ProductFileChooserAction() {
            super("...");
            chooser = new SnapFileChooser();
            chooser.setDialogTitle("Select Source Product");
            final Iterator<ProductReaderPlugIn> iterator = ProductIOPlugInManager.getInstance().getAllReaderPlugIns();
            List<SnapFileFilter> sortedFileFilters = SnapFileFilter.getSortedFileFilters(iterator);
            for (SnapFileFilter fileFilter : sortedFileFilters) {
                chooser.addChoosableFileFilter(fileFilter);
            }
            chooser.setAcceptAllFileFilterUsed(true);
            chooser.setFileFilter(chooser.getAcceptAllFileFilter());
        }

        @Override
        public void actionPerformed(ActionEvent event) {
            final Window window = SwingUtilities.getWindowAncestor((JComponent) event.getSource());

            String homeDirPath = SystemUtils.getUserHomeDir().getPath();
            String openDir = appContext.getPreferences().getPropertyString(OpenProductAction.PREFERENCES_KEY_LAST_PRODUCT_DIR,
                                                                           homeDirPath);
            currentDirectory = new File(openDir);
            chooser.setCurrentDirectory(currentDirectory);

            if (chooser.showDialog(window, APPROVE_BUTTON_TEXT) == JFileChooser.APPROVE_OPTION) {
                SwingWorker cursorWorker = new CursorWorker(window, chooser);
                cursorWorker.execute();
                currentDirectory = chooser.getCurrentDirectory();
                appContext.getPreferences().setPropertyString(OpenProductAction.PREFERENCES_KEY_LAST_PRODUCT_DIR,
                                                              currentDirectory.getAbsolutePath());
            }
        }


    }

    private class CursorWorker extends SwingWorker<Product, Void> {
        private final Component window;
        private final JFileChooser chooser;
        private Cursor defaultCursor;
        private File file;

        public CursorWorker(Component window, JFileChooser chooser) {
            this.window = window;
            this.chooser = chooser;
        }

        @Override
        protected Product doInBackground() throws Exception {
            Product product = null;
            try {
                defaultCursor = window.getCursor();
                window.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
                file = chooser.getSelectedFile();
                product = ProductIO.readProduct(file);
            } catch (IOException e) {
                handleError(e.getMessage());
            }
            return product;
        }

        @Override
        protected void done() {
            Product product = null;
            try {
                product = get();
                if (product == null) {
                    throw new IOException(MessageFormat.format("File ''{0}'' could not be read.", this.file.getPath()));
                }
                if (productFilter.accept(product)) {
                    setSelectedProduct(product);
                } else {
                    final String message = String.format("Product [%s] is not a valid source.",
                                                         product.getFileLocation().getCanonicalPath());
                    handleError(message);
                    product.dispose();
                }

            } catch (InterruptedException | IOException | ExecutionException e) {
                if (product != null) {
                    product.dispose();
                }
                handleError(e.getMessage());
                e.printStackTrace();
            } finally {
                window.setCursor(defaultCursor);
            }
        }

        private void handleError(final String message) {
            SwingUtilities.invokeLater(() -> {
                AbstractDialog.showWarningDialog(window, message, "Error");
            });
        }
    }

    private static class ProductListCellRenderer extends DefaultListCellRenderer {

        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
                                                      boolean cellHasFocus) {
            final Component cellRendererComponent =
                    super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

            if (cellRendererComponent instanceof JLabel) {
                final JLabel label = (JLabel) cellRendererComponent;
                if (value instanceof Product) {
                    final Product product = (Product) value;
                    label.setText(product.getDisplayName());
                } else {
                    label.setText(" ");
                }
            }

            return cellRendererComponent;
        }
    }

    /**
     * To let the popup menu be wider than the closed combobox.
     * Adapted an idea from http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6257236
     */
    private static class ProductPopupMenuListener implements PopupMenuListener {

        @Override
        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            JComboBox box = (JComboBox) e.getSource();
            Object comp = box.getUI().getAccessibleChild(box, 0);
            if (!(comp instanceof JPopupMenu)) {
                return;
            }
            JComponent scrollPane = (JComponent) ((JPopupMenu) comp).getComponent(0);
            Dimension size = new Dimension();
            size.width = scrollPane.getPreferredSize().width;
            final int boxItemCount = box.getModel().getSize();
            for (int i = 0; i < boxItemCount; i++) {
                final JLabel label = new JLabel();
                Object elementAt = box.getModel().getElementAt(i);
                if (elementAt != null && elementAt instanceof Product) {
                    label.setText(((Product) elementAt).getDisplayName());
                }
                size.width = Math.max(label.getPreferredSize().width, size.width);
            }
            size.height = scrollPane.getPreferredSize().height;
            scrollPane.setPreferredSize(size);
            scrollPane.setMaximumSize(size);
        }

        @Override
        public void popupMenuCanceled(PopupMenuEvent e) {
        }

        @Override
        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
        }
    }
}
